/*
 * Copyright 2012 Sedov Eduard.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.es.registry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 *
 * @author Sedov Eduard
 */
public class DefaultRegistry implements Registry {

    private Node root;
    private Node current;
    private RegistryListenerSupport listeners = new RegistryListenerSupport();
    private RootNodeListener rootNodeListener = new RootNodeListener();

    public DefaultRegistry() {
        this(new Node(""));
    }

    public DefaultRegistry(Node root) {
        this.root = root;
        this.root.addListener(rootNodeListener);
        current = root;
    }

    public Node getRoot() {
        return root;
    }

    public void setRoot(Node root) {
        changeRoot(root, root);
    }

    public Node getCurrent() {
        return current;
    }

    public void setCurrent(Node current) {
        changeRoot(root, current);
    }

    public boolean isExist(String name) {
        try {
            String[] path = split(name);
            Node node = cd(getStartNodeOfPath(path), path, false);
            cd(node, path[path.length - 1]);
            return true;
        } catch (IllegalArgumentException e) {
            if (e.getMessage().equals("name not found")) {
                return false;
            } else {
                return true;
            }
        }
    }

    public boolean isDirectory(String name) {
        try {
            cd(name);
            return true;
        } catch (IllegalArgumentException e) {
            if (e.getMessage().equals("name is not directory")) {
                return false;
            } else {
                throw e;
            }
        }
    }

    /**
     * Returns name of current directory.
     *
     * @return name current directory
     */
    public String cd() {
        String result = toPath(current);
        fireOnCd(result);
        return result;
    }

    /**
     * Changes the current directory. Allowed values are:<br> '' or '.' keeps
     * the previous current directory<br> '..' changes to parent directory<br>
     * '/a/b/c' changes to directory 'abc', where 'a' is a child of root<br>
     * 'a/b/c' changes to directory 'abc', where 'a' is a child of current
     * directory<br>
     *
     * @param name the absolute or relative path
     * @return the new current path
     */
    public String cd(String name) {
        String oldName = toPath(current);
        String[] path = split(name);
        current = cd(getStartNodeOfPath(path), path, true);
        String result = toPath(current);
        fireOnCd(oldName, result);
        return result;
    }

    /**
     * Makes directory path specified by name parameter. Allowed are absolute
     * and relative paths.
     *
     * @param name path to create
     */
    public void mkdir(String name) {
        String[] path = split(name);
        rootNodeListener.setQuiet(true);
        Node result = mkdir(getStartNodeOfPath(path), path, true);
        rootNodeListener.setQuiet(false);
        fireOnMkdir(toPath(result));
    }

    /**
     * Removes the directory specified by given name. Allowed are absolute and
     * relative paths.
     *
     * @param name directory to remove
     */
    public void rmdir(String name) {
        String[] path = split(name);
        Node node = cd(getStartNodeOfPath(path), path, true);
        if (node == root) {
            throw new IllegalArgumentException("cannot remove root");
        }
        Node parent = node.getParent();
        if (parent != null) {
            rootNodeListener.setQuiet(true);
            parent.remove(node.getName());
            rootNodeListener.setQuiet(false);
            if (node == current) {
                current = parent;
            }
        }
        fireOnRmdir(node.getName());
    }

    /**
     * Returns the value associated with the given path.
     *
     * @param <T> the type of value
     * @param name the path
     * @return the value or {@code null} if no value set
     */
    public <T> T get(String name) {
        String[] path = split(name);
        Node node = cd(getStartNodeOfPath(path), path, false);
        String valueName = path[path.length - 1];
        T result = (T) node.get(valueName);
        if (result instanceof Node) {
            throw new IllegalArgumentException("cannot get value of path because it is a directory");
        }
        fireOnGet(toPath(node) + valueName, result);
        return result;
    }

    /**
     * Inserts the given value and associates it with the given path.
     *
     * @param <T> the type of previous value
     * @param name the relative or absolute path
     * @param value the value. May not be an instance of {@link Node}.
     * @return the previous value or {@code null} if no such exist
     */
    public <T> T put(String name, T value) {
        if (value instanceof Node) {
            throw new IllegalArgumentException("cannot put a node as value, use mkdir instead");
        }
        String[] path = split(name);
        Node targetNode = mkdir(getStartNodeOfPath(path), path, false);
        String valueName = path[path.length - 1];
        T oldValue = (T) targetNode.get(valueName);
        if (oldValue instanceof Node) {
            throw new IllegalArgumentException("cannot set a value because the name is exist and it is directory");
        }
        rootNodeListener.setQuiet(true);
        targetNode.set(valueName, value);
        rootNodeListener.setQuiet(false);
        fireOnPut(toPath(targetNode) + valueName, oldValue, value);
        return oldValue;
    }

    /**
     * Removes the value associated with the given path.
     *
     * @param <T> the type of value
     * @param name the relative or absolute path
     * @return the removed value or {@code null} if no such exist
     */
    public <T> T delete(String name) {
        String[] path = split(name);
        Node targetNode = cd(getStartNodeOfPath(path), path, false);
        String valueName = path[path.length - 1];
        if (targetNode.isSet(valueName)) {
            if (targetNode.get(valueName) instanceof Node) {
                throw new IllegalArgumentException("the path is directory, use rmdir instead");
            }
            rootNodeListener.setQuiet(true);
            T result = (T) targetNode.remove(valueName);
            rootNodeListener.setQuiet(false);
            fireOnDelete(targetNode + valueName, result);
            return result;
        } else {
            throw new IllegalArgumentException("name not found");
        }
    }

    /**
     * Returns names of children (directories and values) of current directory.
     *
     * @return a set of names
     */
    public Set<String> list() {
        Set<String> result = Collections.unmodifiableSet(current.list());
        fireOnList(result);
        return result;
    }

    /**
     * Renames a directory or value given by {@code oldName}.
     *
     * @param oldName the path to rename
     * @param newName a new name
     */
    public void rename(String oldName, String newName) {
        if (newName == null || newName.isEmpty()) {
            throw new IllegalArgumentException("new name is empty");
        } else {
            String[] tmp = split(newName);
            if (tmp.length > 1 || tmp[0].isEmpty() || tmp[0].equals(DOT) || tmp[0].equals(DOUBLE_DOTS)) {
                throw new IllegalArgumentException("new name is path");
            }
        }
        String[] path = split(oldName);
        Node targetNode = cd(getStartNodeOfPath(path), path, false);
        String nameLastPart = path[path.length - 1];
        if (targetNode.isSet(nameLastPart)) {
            rootNodeListener.setQuiet(true);
            Object value = targetNode.get(nameLastPart);
            if (value instanceof Node) {
                ((Node) value).setName(newName);
            } else {
                targetNode.remove(nameLastPart);
                targetNode.set(newName, value);
            }
            rootNodeListener.setQuiet(false);
            fireOnRename(toPath(targetNode) + nameLastPart, newName);
        } else {
            throw new IllegalArgumentException("name not found");
        }
    }

    /**
     * Puts the given listener to internal listener list.
     *
     * @param listener the listner to put
     */
    public void addListener(RegistryListener listener) {
        listeners.addListener(listener);
    }

    /**
     * Removes the given listener from internal listener list.
     *
     * @param listener the listener to remove
     */
    public void removeListener(RegistryListener listener) {
        listeners.removeListener(listener);
    }

    /**
     * Returns all registered listeners.
     *
     * @return a list with all registered listeners
     */
    public List<RegistryListener> getListeners() {
        return listeners.getListeners();
    }

    /**
     * Splits the given {@code path} around matches of {@link #PATH_DELIMITER}.
     *
     * @param path the path
     * @return the array of strings
     */
    protected String[] split(String path) {
        return path.split(PATH_DELIMITER);
    }

    protected void changeRoot(Node newRoot, Node newCurrent) {
        Node tmp = newCurrent;
        while (tmp != root && tmp.getParent() != null) {
            tmp = tmp.getParent();
        }
        if (tmp != root) {
            newRoot = tmp;
        }

        if (newRoot != root) {
            root.removeListener(rootNodeListener);

            for (String name : root.list()) {
                Object obj = root.get(name);
                if (obj instanceof Node) {
                    fireOnRmdir("/" + name);
                } else {
                    fireOnDelete("/" + name, obj);
                }
            }


            for (String name : newRoot.list()) {
                Object obj = newRoot.get(name);
                if (obj instanceof Node) {
                    fireOnMkdir("/" + name);
                } else {
                    fireOnPut("/" + name, null, obj);
                }
            }

            newRoot.addListener(rootNodeListener);

            root = newRoot;
        }

        current = newRoot;

        cd(toPath(newCurrent));
    }

    protected Node cd(Node node, String[] chunks, boolean shouldLastNode) {
        int lastIndex = shouldLastNode ? chunks.length - 1 : chunks.length - 2;
        for (int i = 0; i <= lastIndex; i++) {
            String name = chunks[i];
            node = cd(node, name);
        }
        return node;
    }

    protected Node cd(Node node, String name) {
        if (name.equals(DOT) || name.equals("")) {
            return node;
        } else if (name.equals(DOUBLE_DOTS)) {
            if (node == root) {
                throw new IllegalArgumentException("cannot execute '" + DOUBLE_DOTS + "' on root");
            } else {
                return node.getParent();
            }
        } else if (node.isSet(name)) {
            Object value = node.get(name);
            if (value instanceof Node) {
                return (Node) value;
            } else {
                throw new IllegalArgumentException("name is not directory");
            }
        } else {
            throw new IllegalArgumentException("name not found");
        }
    }

    protected Node mkdir(Node node, String[] chunks, boolean shouldLastNode) {
        int lastIndex = shouldLastNode ? chunks.length - 1 : chunks.length - 2;
        for (int i = 0; i <= lastIndex; i++) {
            String chunk = chunks[i];
            if (node.isSet(chunk) || chunk.equals("") || chunk.equals(DOUBLE_DOTS) || chunk.equals(DOT)) {
                node = cd(node, chunk);
            } else {
                node = new Node(chunk, node);
            }
        }
        return node;
    }

    /**
     * Builds a string representation of given {@code node}. The resulting
     * string starts and ends with {@link #PATH_DELIMITER} (e.g. '/a/b/c/' ).
     *
     * @param node the node
     * @return string path
     */
    protected String toPath(Node node) {
        StringBuilder sb = new StringBuilder();
        List<Node> path = new ArrayList();
        while (node != null && node != root) {
            path.add(node);
            node = node.getParent();
        }
        Collections.reverse(path);
        sb.append(PATH_DELIMITER);
        for (Node n : path) {
            sb.append(n.getName());
            sb.append(PATH_DELIMITER);
        }
        return sb.toString();
    }

    /**
     * Return the first node of given path. If the path is an absolute path
     * returns the root node otherwise the current node.
     *
     * @param path the path
     * @return the first node of the given path
     */
    protected Node getStartNodeOfPath(String[] path) {
        if (path.length == 0 || path[0].equals("")) {
            // if name is "/" or "/XYZ" it is the root node
            return root;
        } else {
            // otherwise current node
            return current;
        }
    }

    protected void fireOnCd(String currentName) {
        listeners.fireOnCd(this, currentName);
    }

    protected void fireOnCd(String oldName, String newName) {
        listeners.fireOnCd(this, oldName, newName);
    }

    protected void fireOnMkdir(String name) {
        listeners.fireOnMkdir(this, name);
    }

    protected void fireOnRmdir(String name) {
        listeners.fireOnRmdir(this, name);
    }

    protected void fireOnGet(String name, Object value) {
        listeners.fireOnGet(this, name, value);
    }

    protected void fireOnPut(String name, Object oldValue, Object newValue) {
        listeners.fireOnPut(this, name, oldValue, newValue);
    }

    protected void fireOnDelete(String name, Object value) {
        listeners.fireOnDelete(this, name, value);
    }

    protected void fireOnList(Set<String> result) {
        listeners.fireOnList(this, result);
    }

    protected void fireOnRename(String oldName, String newName) {
        listeners.fireOnRename(this, oldName, newName);
    }

    protected class RootNodeListener implements BubbleNodeListener {

        private boolean quiet = false;

        public boolean isQuiet() {
            return quiet;
        }

        public void setQuiet(boolean quiet) {
            this.quiet = quiet;
        }

        @Override
        public void onName(Node node, String oldName, String newName) {
            if (isQuiet()) {
                return;
            }
            fireOnRename(toPath(node.getParent()) + oldName + PATH_DELIMITER, newName);
        }

        @Override
        public void onParent(Node node, Node oldParent, Node newParent) {
        }

        @Override
        public void onSet(Node node, String name, Object oldValue, Object newValue) {
            if (isQuiet()) {
                return;
            }
            if (oldValue == null && newValue instanceof Node) {
                fireOnMkdir(toPath((Node) newValue));
            } else {
                fireOnPut(toPath(node) + name, oldValue, newValue);
            }
        }

        @Override
        public void onRemove(Node node, String name, Object value) {
            if (isQuiet()) {
                return;
            }
            if (value instanceof Node) {
                fireOnRmdir(toPath(node) + name + PATH_DELIMITER);
            } else {
                fireOnDelete(toPath(node) + name, value);
            }
        }
    }
}
